 // The main execution module
/////////////////////////////////////////////////////////////////////////

// import Robot Karel
import Robot = require("./RobotKarel");

// import instructions
import Instr = require("./Instructions");


// handler for requesting city redraw
export interface ICityRedrawRequested {
    (): void;
}

// reporting robot Karel's exception
export interface IKarelExceptionHandler {
    (type: Instr.eKarelException): void;
}

// command completed handler
export interface ICommandCompleted {
    (): void;
}

// execution context class
class Context {
    public CurrentInstructionSet: Instr.InstructionSet;
    public InstructionIndex: number;
    public Registers: Array<number>

    // constructor
    constructor() {
        this.CurrentInstructionSet = null;
        this.InstructionIndex = 0;
        this.Registers = new Array<number>();
    }
}

// the main command execution class
export class ExecutionEngine implements Instr.IExecutionEngine {
    
    // singleton instance
    private static mInstance: ExecutionEngine = null;

    // execution variables
    private mIsRunning: boolean;
    private mIsPaused: boolean;
    private mInterval: number;

    // reference to the Karel's class
    private mKarel: Robot.RobotKarel;

    // registers
    private mRegisters: Array<number>;

    // the context stack
    private mContextStack: Array<Context>;
    private mCurrentContext: Context;
    private mMaxContextStack: number;

    // compiled commands = instruction sets
    private mInstructionSets: Array<Instr.InstructionSet>;

    private mRedrawEvent: ICityRedrawRequested;
    private mKarelExceptionEvent: IKarelExceptionHandler;
    private mExecutionCompleted: ICommandCompleted;

    // constructor
    public constructor() {
        // set reference to the robot class
        this.mKarel = Robot.RobotKarel.Instance;

        // init registers
        this.mRegisters = new Array<number>();

        // init context stack
        this.mContextStack = new Array<Context>();
        this.mCurrentContext = null;
        this.mMaxContextStack = 100;

        // alloc instruction sets
        this.mInstructionSets = new Array<Instr.InstructionSet>();

        // set default values
        this.mIsRunning = false;
        this.mIsPaused = false;
        this.mInterval = 500;

        this.mRedrawEvent = null;
        this.mKarelExceptionEvent = null;
        this.mExecutionCompleted = null;
    }

    // resets the execution engine
    public Reset(): void {
        this.mRegisters = new Array<number>();
        this.mContextStack = new Array<Context>();
        this.mInstructionSets = new Array<Instr.InstructionSet>();
        this.mCurrentContext = null;
    }

    // set the redraw event
    public SetRedrawHandler(handler: ICityRedrawRequested) {
        this.mRedrawEvent = handler;
    }

    // set Karel's exception handler
    public SetKarelExceptionHandler(handler: IKarelExceptionHandler) {
        this.mKarelExceptionEvent = handler;
    }

    // set execution completed event handler
    public SetExecutionCompleted(handler: ICommandCompleted) {
        this.mExecutionCompleted = handler;
    }

    // property getter creating and returning the singleton instance
    public static get Instance() {
        if (ExecutionEngine.mInstance == null)
            ExecutionEngine.mInstance = new ExecutionEngine();
        return ExecutionEngine.mInstance;
    }

    // the main API method to run the command
    public RunCommand(commandName: string) {
        // get the compiled instruction set
        var instrSet = this.GetInstructionSetByName(commandName);
        if (instrSet == null)
            throw "Passed command to call not found.";

        // init registers
        this.InitRegisterArray(instrSet.RegisterCount);

        // create the context
        this.mCurrentContext = new Context();
        this.mCurrentContext.CurrentInstructionSet = instrSet;

        // set running
        this.mIsRunning = true;
        this.mIsPaused = false;

        // start the execution timer
        window.setTimeout(this.ExecuteTick, 1, this);
    }

    // the main execution method
    public ExecuteTick(This: ExecutionEngine) {
        // check if not has been terminated
        if (This.IsRunning == false || This.IsPaused == true)
            return;

        try {
            // iterate instructions until the next execution tick requested
            while (true) {
                // get the instruction set
                var instrSet = This.mCurrentContext.CurrentInstructionSet;

                // check the end of execution
                while (This.mCurrentContext.InstructionIndex >= instrSet.InstructionCount) {
                    
                    // check the end of execution           
                    if (This.mContextStack.length == 0) {
                        This.mIsRunning = false;

                        // invoke execution completed event
                        if (This.mExecutionCompleted != null) {
                            This.mExecutionCompleted();
                        }

                        return;
                    }

                    // pop context
                    This.PopContext();

                    // get the instruction set
                    var instrSet = This.mCurrentContext.CurrentInstructionSet;
                }

                // get the current instruction
                var instruction = instrSet.GetInstruction(This.mCurrentContext.InstructionIndex);

                // execute the command
                var result = instruction.Execute(This.mKarel, This);
                if (result == true) {
                    // check if not paused
                    if (This.IsPaused)
                        break;

                    // check redraw event
                    if (This.mRedrawEvent != null) {
                        This.mRedrawEvent();
                    }

                    // if the next execution tick requested
                    window.setTimeout(This.ExecuteTick, This.mInterval, This);
                    break;
                }
            }
        }
        catch (e) {
            // check Karel's exception
            if (e instanceof Instr.KarelException) {
                // check handler
                if (This.mKarelExceptionEvent != null) {
                    // invoke the handler if set
                    This.mKarelExceptionEvent((<Instr.KarelException>e).ExceptionType);
                }
            }
            else {
                throw e;
            }
        }
    }

    // returns whether a command is running
    public get IsRunning(): boolean {
        return this.mIsRunning;
    }

    // returns whether a command is paused
    public get IsPaused(): boolean {
        return this.mIsPaused;
    }

    // returns the maximum value of the stack
    public get MaxStack(): number {
        return this.mMaxContextStack;
    }

    // sets the execution speed interval
    public SetInterval(interval: number) {
        this.mInterval = interval;
    }

    // returns the execution speed interval
    public GetInterval(): number {
        return this.mInterval;
    }

    // sets the maximum stack value
    public SetMaxContextStack(max: number): void {
        this.mMaxContextStack = max;
    }

    // pauses the execution
    public Pause(): void {
        if (this.IsRunning)
            this.mIsPaused = true;
    }

    // resumes the execution
    public Resume(): void {
        this.mIsPaused = false;
        if (this.mIsRunning) {
            // restore the timer
            window.setTimeout(this.ExecuteTick, this.mInterval, this);
        }
    }

    // terminates the engine
    public Terminate(): void {
        this.mIsRunning = false;
        this.mIsPaused = false;
        this.Reset();
    }

    // returns index of a new register
    public RequestRegister(): number {
        var idx = this.mRegisters.length;
        this.mRegisters.push(0);
        return idx;
    }

    // sets the register
    public SetRegister(index: number, value: number): void {
        this.mRegisters[index] = value;
    }

    // returns the register value
    public GetRegister(index: number): number {
        return this.mRegisters[index];
    }

    // saves registers to the context
    private PushRegisters(context: Context): void {
        context.Registers = new Array<number>();

        for (var i = 0; i < this.mRegisters.length; i++) {
            context.Registers.push(this.mRegisters[i]);
        }
    }

    // restores registers from the context
    private PopRegisters(context: Context): void {
        this.mRegisters = new Array<number>();
        for (var i = 0; i < context.Registers.length; i++) {
            this.mRegisters.push(context.Registers[i]);
        }
    }

    // resets register values
    private ClearRegisters(): void {
        for (var i = 0; i < this.mRegisters.length; i++) {
            this.mRegisters[i] = 0;
        }
    }

    // allocates the registers' array
    private InitRegisterArray(count: number) {
        this.mRegisters = new Array<number>(count);
        for (var i = 0; i < count; i++) {
            this.mRegisters[i] = 0;
        }
    }

    // saves the current context to the stack
    private PushContext(): void {
        // save registers to the current context
        this.PushRegisters(this.mCurrentContext);

        // push to the context
        this.mContextStack.push(this.mCurrentContext);

        // check the stack size
        if (this.mContextStack.length >= this.mMaxContextStack) {
            // if overflow, throw exception
            this.RaiseException(Instr.eKarelException.Tired);
        }
    }

    // restores the context
    private PopContext(): void {
        // check the stack empty
        if (this.mContextStack.length == 0)
            throw "No Context to be popped.";

        // pop the context
        this.mCurrentContext = this.mContextStack.pop();

        // pop register
        this.PopRegisters(this.mCurrentContext);
    }

    public CallCommand(name: string): void {
        // get the compiled instruction set
        var instrSet = this.GetInstructionSetByName(name);
        if (instrSet == null)
            throw "Passed command to call not found.";

        // push context
        this.PushContext();

        // init registers
        this.InitRegisterArray(instrSet.RegisterCount);

        // create the context
        this.mCurrentContext = new Context();
        this.mCurrentContext.CurrentInstructionSet = instrSet;
    }

    // returns the instruction set by the name
    private GetInstructionSetByName(commandName: string): Instr.InstructionSet {
        commandName = commandName.toLocaleLowerCase();
        for (var i = 0; i < this.mInstructionSets.length; i++) {
            var instrSet = this.mInstructionSets[i];
            if (instrSet.Name.toLocaleLowerCase() == commandName)
                return instrSet;
        }

        return null;
    }

    // moves the instruction pointer
    public MoveInstructionIndex(moveOffset: number): void {
        this.mCurrentContext.InstructionIndex += moveOffset;
    }

    // raises the Karel's exception
    public RaiseException(exceptionType: Instr.eKarelException) {
        // stop execution
        this.mIsRunning = false;

        // throw exception
        throw new Instr.KarelException(exceptionType);
    }

    // checks, whether the command is compiled
    public IsCommandCompiled(commandName: string): boolean {
        for (var i = 0; i < this.mInstructionSets.length; i++) {
            if (this.mInstructionSets[i].Name == commandName)
                return true;
        }

        return false;
    }

    // invokes the compilation process of a command
    public StartCompilation(commandName: string): Instr.InstructionSet {
        // init the context
        if(this.mCurrentContext != null) {
            this.PushContext();
        }
        this.mCurrentContext = new Context();        

        // clears the register array
        this.InitRegisterArray(0);
        // create a new instruction set
        var instrSet = new Instr.InstructionSet(commandName);
        // add to container
        this.mInstructionSets.push(instrSet);

        return instrSet;
    }

    // completes the compilation
    public FinalizeCompilation(instructionSet: Instr.InstructionSet) {
        // set the register count
        instructionSet.SetRegisterCount(this.mRegisters.length);

        // restore the context
        if (this.mContextStack.length > 0) {
            this.PopContext();
        }
    }
}
